﻿using System;
using System.Collections.Generic;
using System.Linq;
using ArivisTests.Models;

namespace ArivisTests.Controllers
{
    class AssignmentUser
    {
        // the associated user profile
        public UserProfile UserProfile;
     
        // the availability for the user
        public int Available;

        public override string ToString()
        {
            return "Id:" + UserProfile.UserId + ", Name:" + UserProfile.UserName + ", Available:" + Available;
        }
    }

    class AssignmentScenario
    {
        public TestScenario Scenario;
        public List<TestCase> Tests;
        public int Runs;
    }

    class AssignmentCase
    {
        private AssignmentCase(TestCase testCase)
        {
            this.TestCase = testCase;
            this.Duration = testCase.Scenario.Duration;
            this.Owner = testCase.Scenario.Owner;
        }

        public static AssignmentCase Create(TestCase testCase)
        {
            return new AssignmentCase(testCase);
        }

        public readonly int Duration;

        public readonly UserProfile Owner;

        public readonly TestCase TestCase;

        public TestScenario TestScenario
        {
            get
            {
                return TestCase.Scenario;
            }
        }

        public override string ToString()
        {
            return "Case:" + TestCase.Name + ", Owner:" + (Owner == null ? "null" : Owner.UserName);
        }
    
    }

    class Assignment
    {
        // the test users
        public AssignmentUser User;

        public UserProfile UserProfile { get { return User.UserProfile; } }

        // Sum(User.Time)
        public int SumTime;
        // Sum(Test.Duration)
        public int SumDuration;

        // test assigned to the user
        public List<AssignmentCase> Tests = new List<AssignmentCase>();

        // duration of assigned tests for this user
        public int Duration { get { return Tests.Sum(x => x.Duration); } }

        // true if user has tests assigned where user is owner
        public int SelfTests
        {
            get
            {
                return this.Tests.Count(t => t.Owner == User.UserProfile);
            }
        }

        // error of actual assigned test duration relative to targeted duration based on User.Time in percent
        public float Error
        {
            get
            {
                return getError(this.Duration);
            }
        }

        public float TestErrorAbs(int durationDelta)
        {
            return Math.Abs(getError(this.Duration + durationDelta));
        }

        private float getError(int duration)
        {
            float targetDuration = (User.Available * SumDuration) / (float)SumTime;
            return (duration / targetDuration - 1f);
        }

        public float ErrorAbs
        {
            get
            {
                return Math.Abs(this.Error);
            }
        }

        public bool Contains(TestScenario scenario)
        {
            return Tests.Any(test => test.TestScenario == scenario);
        }

        public override string ToString()
        {
            return User.UserProfile.UserName + ", Tests: " + Tests.Count;
        }

    }

    class AssignTestImpl
    {
        public static List<Assignment> AssignTester(List<AssignmentUser> users, List<AssignmentScenario> scenarios)
        {
            // sanity check
            if (users == null || users.Count == 0 || scenarios == null || scenarios.Count == 0)
                return new List<Assignment>();

            // get all tests
            List<AssignmentCase> tests = scenarios.SelectMany(x => x.Tests.Select(AssignmentCase.Create)).ToList();

            // get total amount of testing time and user availability
            int sumAvailable = users.Sum(x => x.Available);
            int sumDuration = tests.Sum(x => x.Duration);

            // create assignments for all users
            var assignments = users.Select(u => new Assignment { User = u, SumTime = sumAvailable, SumDuration = sumDuration }).ToList();
            var shuffeledTests = Shuffel(tests);

            // creata initial assignment of tests
            foreach (var test in shuffeledTests)
            {
                var assignTo = assignments
                    .Where(assignment => assignment.UserProfile != test.Owner) // don't assign to owner
                    .Where(assignment => !assignment.Contains(test.TestScenario)) // don't assign same scenario
                    .OrderBy(assignment => assignment.Error) // lowest error first
                    .FirstOrDefault(); // take first available

                if (assignTo != null)
                {
                    assignTo.Tests.Add(test);
                }
            }

            FixSelfTests(assignments);
            while (Optimize(assignments)) { }

            return assignments;
        }

        private static List<T> Shuffel<T>(IEnumerable<T> items)
        {
            List<T> input = items.ToList();
            Random rand = new Random();
            List<T> output = new List<T>();
            while (input.Count > 0)
            {
                int index = rand.Next(input.Count);
                output.Add(input[index]);
                input.RemoveAt(index);
            }
            return output;
        }

        private static void FixSelfTests(List<Assignment> assignments)
        {
            // one user just has to do everything
            if (assignments.Count < 2)
                return;

            foreach (var source in assignments)
            {
                var others = assignments.Where(x => x != source).ToList();
                var selfTests = source.Tests.Where(t => t.Owner == source.User.UserProfile).ToList();
                foreach (var test in selfTests)
                {
                    var target = others.OrderBy(x => x.Error).First();
                    source.Tests.Remove(test);
                    target.Tests.Add(test);
                }
            }
        }

        private static bool Optimize(List<Assignment> assignments)
        {
            var byError = assignments.OrderByDescending(x => x.Error).ToList();

            // try to optimize start with biggest error
            foreach (var ass in byError)
            {
                // check others start with smallest error
                var others = assignments.Where(x => x != ass).OrderBy(x => x.Error).ToList();
                foreach (var other in others)
                {
                    if (Optimize(ass, other)) return true;
                }
            }
            return false;
        }

        private static bool Optimize(Assignment source, Assignment target)
        {
            float beforeError = source.ErrorAbs + target.ErrorAbs;

            float bestAfterError = beforeError;
            AssignmentCase bestSwapTest = null;

            foreach (var test in source.Tests)
            {
                // check if test can be given to target
                if (target.Tests.Contains(test) || target.UserProfile == test.Owner)
                    continue;

                // check error if test is given to target
                float afterError = source.TestErrorAbs(-test.Duration) + target.TestErrorAbs(test.Duration);

                // swap if this is an improvement
                if (afterError < bestAfterError)
                {
                    bestAfterError = afterError;
                    bestSwapTest = test;
                }
            }

            if (bestSwapTest != null)
            {
                source.Tests.Remove(bestSwapTest);
                target.Tests.Add(bestSwapTest);
                return true;
            }
            else
            {
                return false;
            }
        }

    }
}